package main

import (
	"bufio"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"mime"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/shopspring/decimal"
)

func main() {
	startTime := time.Now()

	// 从标准输入生成读对象
	reader := bufio.NewReader(os.Stdin)
	fmt.Print("请输入url：")
	// 读到换行或回车结束读取
	url, err := reader.ReadString('\n')
	// 由于错误时return了,下面使用url就会报错;所以不能边定义边判错
	if err != nil {
		fmt.Printf("Error：%#v\n", err)
		return
	}
	// 去除前后空格，得到下载地址
	// url = "https://download.jetbrains.com/go/goland-2020.2.2.dmg"
	url = strings.TrimSpace(url)
	// golandHash := "3af4660ef22f805008e6773ac25f9edbc17c2014af18019b7374afbed63d4744" // goland-2020.2.2.dmg官方公布的文件hash值:
	downloader := NewFileDownloader(url, "", "", "", 10)
	if err := downloader.Run(); err != nil {
		// fmt.Printf("\n%s", err)
		log.Fatal(err)
	}
	totalTimeSpent := time.Now().Sub(startTime).Seconds()
	averageDownloadSpeed := decimal.NewFromFloat(float64(downloader.fileSize)).Div(decimal.NewFromFloat(totalTimeSpent))
	fmt.Printf("\n文件下载完成总耗时: %f second，文件总大小：%v，平均下载速度：%v byte/s。\n", totalTimeSpent, downloader.fileSize, averageDownloadSpeed)
}

//filePart 文件分片
type filePart struct {
	Index int    // 文件分片的序号
	From  int    // 开始byte
	To    int    // 解决byte
	Data  []byte // http下载得到的文件内容
}

// FileDownloader 文件下载器 结构体
type FileDownloader struct {
	fileSize       int
	url            string
	outputFileName string // 输出文件名
	outputDir      string // 输出目标文件夹
	fileHash       string // 文件唯一哈希值,判断是不是原版文件
	totalPart      int    // 下载线程
	doneFilePart   []filePart
}

// NewFileDownloader 实例化一个下载器
/**
* @params fileHash {string} 为空则不校验hash
 */
func NewFileDownloader(url, outputFileName, outputDir, fileHash string, totalPart int) *FileDownloader {
	if outputDir == "" {
		wd, err := os.Getwd() //获取当前工作目录
		if err != nil {
			log.Println(err)
		}
		outputDir = wd
	}
	return &FileDownloader{
		fileSize:       0,
		url:            url,
		outputFileName: outputFileName,
		outputDir:      outputDir,
		totalPart:      totalPart,
		fileHash:       fileHash,
		doneFilePart:   make([]filePart, totalPart),
	}
}

//Run 结构体实现方法: 开始下载任务
func (d *FileDownloader) Run() error {
	fileTotalSize, err := d.head()
	if err != nil {
		return err
	}
	d.fileSize = fileTotalSize

	jobs := make([]filePart, d.totalPart)
	// 每个线程的下载大小
	eachSize := fileTotalSize / d.totalPart

	// 遍历文件单元切片
	for i := range jobs {
		jobs[i].Index = i
		if i == 0 {
			jobs[i].From = 0
		} else {
			jobs[i].From = jobs[i-1].To + 1
		}
		if i < d.totalPart-1 {
			jobs[i].To = jobs[i].From + eachSize
		} else {
			//the last filePart
			jobs[i].To = fileTotalSize - 1
		}
	}

	// 下载文件单元
	var wg sync.WaitGroup
	for _, j := range jobs {
		wg.Add(1)
		go func(job filePart) {
			defer wg.Done()
			err := d.downloadPart(job)
			if err != nil {
				log.Println("下载文件单元失败:", err, job)
			}
		}(j)
	}
	fmt.Println("/* 文件并行下载,下载顺序不一致! */")
	wg.Wait()
	return d.mergeFileParts()
}

//head 结构体实现方法: 根据响应头获取要下载的文件的基本信息(header) 使用HTTP Method Head
func (d *FileDownloader) head() (int, error) {
	r, err := d.getNewRequest("HEAD")
	if err != nil {
		return 0, err
	}
	resp, err := http.DefaultClient.Do(r)
	if err != nil {
		return 0, err
	}
	if resp.StatusCode > 299 {
		// return 0, errors.New(fmt.Sprintf("Can't process, response is %v", resp.StatusCode))
		return 0, fmt.Errorf("Can't process, response is %v", resp.StatusCode)
	}
	//检查是否支持 断点续传
	//https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Ranges
	if resp.Header.Get("Accept-Ranges") != "bytes" {
		return 0, errors.New("服务器不支持文件断点续传")
	}

	d.outputFileName = parseFileInfoFrom(resp)
	//https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length
	return strconv.Atoi(resp.Header.Get("Content-Length"))
}

// parseFileInfoFrom 获取文件名
func parseFileInfoFrom(resp *http.Response) string {
	contentDisposition := resp.Header.Get("Content-Disposition")
	if contentDisposition != "" {
		_, params, err := mime.ParseMediaType(contentDisposition)

		if err != nil {
			panic(err)
		}
		return params["filename"]
	}
	filename := filepath.Base(resp.Request.URL.Path)
	return filename
}

//下载分片
func (d FileDownloader) downloadPart(c filePart) error {
	r, err := d.getNewRequest("GET")
	if err != nil {
		return err
	}
	log.Printf("开始切片单元[%d]下载... 下载区间：[%d, %d]\n", c.Index, c.From, c.To)
	r.Header.Set("Range", fmt.Sprintf("bytes=%v-%v", c.From, c.To))
	resp, err := http.DefaultClient.Do(r)
	if err != nil {
		return err
	}
	if resp.StatusCode > 299 {
		return fmt.Errorf("服务器错误状态码: %v", resp.StatusCode)
	}
	defer resp.Body.Close()
	bs, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	if len(bs) != (c.To - c.From + 1) {
		return errors.New("下载文件分片长度错误(^_−)")
	}
	c.Data = bs
	d.doneFilePart[c.Index] = c
	return nil

}

// getNewRequest 创建一个request
func (d FileDownloader) getNewRequest(method string) (*http.Request, error) {
	r, err := http.NewRequest(
		method,
		d.url,
		nil,
	)
	if err != nil {
		return nil, err
	}
	r.Header.Set("User-Agent", "mojocn")
	return r, nil
}

//mergeFileParts 合并下载的文件
func (d FileDownloader) mergeFileParts() error {
	log.Println("开始合并文件...")
	path := filepath.Join(d.outputDir, d.outputFileName)
	mergedFile, err := os.Create(path)
	if err != nil {
		return err
	}
	defer mergedFile.Close()
	hash := sha256.New()
	totalSize := 0
	for _, s := range d.doneFilePart {

		mergedFile.Write(s.Data)
		hash.Write(s.Data)
		totalSize += len(s.Data)
	}
	if totalSize != d.fileSize {
		return errors.New("文件不完整(^_−)")
	}
	//https://download.jetbrains.com/go/goland-2020.2.2.dmg.sha256?_ga=2.223142619.1968990594.1597453229-1195436307.1493100134
	if d.fileHash != "" {
		if hex.EncodeToString(hash.Sum(nil)) != d.fileHash {
			return errors.New("hash值不匹配，文件已损坏(^_−)")
		}
	}
	log.Println("文件SHA-256校验成功!")
	return nil

}
